package com.simpou.commons.persistence.jpa.dao.impl;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;

import com.simpou.commons.model.entity.BaseEntity;
import com.simpou.commons.model.entity.IdentifiableEntity;
import com.simpou.commons.persistence.common.Cache;
import com.simpou.commons.persistence.common.Transaction;
import com.simpou.commons.persistence.dao.TypedDAO;
import com.simpou.commons.persistence.jpa.model.SingularAttributeEntry;
import com.simpou.commons.utils.pagination.PageLimits;
import com.simpou.commons.utils.validation.Assertions;


/**
 * Gerenciador genérico de entidades. Classe padrão para acesso e manipulação de
 * entidades persistidas. Leva em consideração a tipagem da classe em todas as
 * operações.
 * 
 * @param <T>
 *            Tipo das entidades a serem manipuladas/acessadas.
 * @author Jonas Pereira
 * @since 2011-08-19
 * @version 2013-06-01
 */
public abstract class AbstractTypedJpaDAOImpl<T extends BaseEntity>
implements TypedDAO<T> {
	/**
	 * Ações compatíveis serão delegadas a esta instância. Herança não é
	 * utilizada para garantir a tipagem segura.
	 */
	private final AbstractJpaDAOImpl daoDelegate = new AbstractJpaDAOImpl() {
		@Override
		protected EntityManager getEntityManager() {
			return getEntityManagerProxy();
		}
	};

	/**
	 * Classe da entidade parametrizada pela subclasse.
	 */
	private final Class<T> entityClass;

	/**
	 * <p>Constructor for AbstractJpaDAOImpl.</p>
	 *
	 * @param entityClass a {@link java.lang.Class} object.
	 */
	public AbstractTypedJpaDAOImpl(final Class<T> entityClass) {
		this.entityClass = Assertions.notNull(entityClass);
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public T create(final T entity) throws Exception {
		return this.daoDelegate.create(entity);
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void delete(final T entity) throws Exception {
		this.daoDelegate.delete(entity);
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public T update(final T entity) throws Exception {
		return this.daoDelegate.update(entity);
	}

	@Override
	public <E extends Serializable, T extends IdentifiableEntity<E>> T getSingle(
			final Class<T> clasz, final E id) throws Exception {
		return this.daoDelegate.getSingle(clasz, id);
	}

	@Override
	public <E extends Serializable, T extends IdentifiableEntity<E>> void delete(
			final Class<T> clasz, final E id) throws Exception {
		this.daoDelegate.delete(clasz, id);
	}

	/**
	 * {@inheritDoc}
	 */
	 @Override
	 public List<T> getList() throws Exception {
		 return this.daoDelegate.getList(this.entityClass);
	 }

	 /**
	  * {@inheritDoc}
	  */
	 @Override
	 public Long count() throws Exception {
		 return this.daoDelegate.count(this.entityClass);
	 }

	 /**
	  * {@inheritDoc}
	  */
	 @Override
	 public List<T> getList(final PageLimits limits) throws Exception {
		 return this.daoDelegate.getList(this.entityClass, limits);
	 }

	 /**
	  * {@inheritDoc}
	  */
	 @Override
	 public int execute(final String stringQuery, final Object... params)
			 throws Exception {
		 return this.daoDelegate.execute(stringQuery, params);
	 }

	 @Override
	 public int namedExecute(final String namedQuery, final Object... params)
			 throws Exception {
		 return this.daoDelegate.namedExecute(namedQuery, params);
	 }

	 @Override
	public <E> E getSingle(final Class<E> clasz,
			 final String stringQuery, final Object... params) throws Exception {
		 return this.daoDelegate.getSingle(clasz, stringQuery, params);
	 }

	/**
	 * {@inheritDoc}
	 */
	 @Override
	public <E> List<E> getList(final Class<E> objClass,
			final String stringQuery, final PageLimits limits,
			final Object... params) throws Exception {
		return this.daoDelegate.getList(objClass, stringQuery, limits, params);
	}

	@Override
	 public T getSingle(final String stringQuery, final Object... params)
			 throws Exception {
		 return this.daoDelegate.getSingle(this.entityClass, stringQuery, params);
	 }

	 @Override
	 public T getNamedSingle(final String namedQuery, final Object... params)
			 throws Exception {
		 return this.daoDelegate.getNamedSingle(this.entityClass, namedQuery, params);
	 }

	 /**
	  * {@inheritDoc}
	  */
	 @Override
	 public Long count(final String stringQuery, final Object... params)
			 throws Exception {
		 return this.daoDelegate.count(stringQuery, params);
	 }

	 @Override
	 public List<T> getList(final String stringQuery, final PageLimits limits,
			 final Object... params) throws Exception {
		 return getList(this.entityClass, stringQuery, limits, params);
	 }

	 @Override
	 public List<T> getNamedList(final String namedQuery, final PageLimits limits,
			 final Object... params) throws Exception {
		 return this.daoDelegate.getNamedList(this.entityClass, namedQuery, limits, params);
	 }

	 /**
	  * {@inheritDoc}
	  */
	 @Override
	 public void deleteAll() throws Exception {
		 this.daoDelegate.deleteAll(this.entityClass);
	 }

	 /**
	  * <p>getSingle.</p>
	  *
	  * @param equalAttributes a {@link java.util.List} object.
	  * @param likeAttributes a {@link java.util.List} object.
	  * @return a T object.
	  * @throws java.lang.Exception if any.
	  */
	 public T getSingle(
			 final List<SingularAttributeEntry<T, ?>> equalAttributes,
			 final List<SingularAttributeEntry<T, String>> likeAttributes)
					 throws Exception {
		 final TypedQuery<T> query = getQuery(null, false, equalAttributes,
				 likeAttributes);

		 return getSingle(query);
	 }

	 /**
	  * <p>count.</p>
	  *
	  * @param equalAttributes a {@link java.util.List} object.
	  * @param likeAttributes a {@link java.util.List} object.
	  * @return a {@link java.lang.Long} object.
	  * @throws java.lang.Exception if any.
	  */
	 public Long count(
			 final List<SingularAttributeEntry<T, ?>> equalAttributes,
			 final List<SingularAttributeEntry<T, String>> likeAttributes)
					 throws Exception {
		 return getQuery(this.entityClass, Long.class, null, true, equalAttributes,
				 likeAttributes).getSingleResult();
	 }

	 /**
	  * <p>getList.</p>
	  *
	  * @param equalAttributes a {@link java.util.List} object.
	  * @param likeAttributes a {@link java.util.List} object.
	  * @return a {@link java.util.List} object.
	  * @throws java.lang.Exception if any.
	  * @since 0.1
	  */
	 public List<T> getList(
			 final List<SingularAttributeEntry<T, ?>> equalAttributes,
			 final List<SingularAttributeEntry<T, String>> likeAttributes)
					 throws Exception {
		 return getList(null, equalAttributes, likeAttributes);
	 }

	 /**
	  * <p>getList.</p>
	  *
	  * @param limits a {@link PageLimits}
	  * object.
	  * @param equalAttributes a {@link java.util.List} object.
	  * @param likeAttributes a {@link java.util.List} object.
	  * @return a {@link java.util.List} object.
	  * @throws java.lang.Exception if any.
	  */
	 public List<T> getList(final PageLimits limits,
			 final List<SingularAttributeEntry<T, ?>> equalAttributes,
			 final List<SingularAttributeEntry<T, String>> likeAttributes)
					 throws Exception {
		 return getQuery(limits, false, equalAttributes, likeAttributes)
				 .getResultList();
	 }

	 /**
	  * <p>getEntityManager.</p>
	  *
	  * @return a {@link javax.persistence.EntityManager} object.
	  */
	 protected abstract EntityManager getEntityManager();

	 /**
	  * Diferencia o getter do da classe delegada.
	  *
	  * @return EntityManager.
	  */
	 private EntityManager getEntityManagerProxy() {
		 return getEntityManager();
	 }

	 /**
	  *
	  * @param objClass Classe da entidade.
	  * @param query Query pronta.
	  * @return Resultado da query.
	  * @throws Exception Se houver.
	  */
	 protected <E> E getSingle(final Class<E> objClass, final TypedQuery<E> query)
			 throws Exception {
		 return this.daoDelegate.getSingle(objClass, query);
	 }

	 /**
	  * @param query Query pronta.
	  * @return Resultado da query.
	  * @throws Exception Se houver.
	  */
	 protected T getSingle(final TypedQuery<T> query) throws Exception {
		 return getSingle(this.entityClass, query);
	 }

	 @Override
	 public void flush() throws Exception {
		 this.daoDelegate.flush();
	 }

	 @Override
	 public Transaction getTransaction() {
		 return this.daoDelegate.getTransaction();
	 }

    @Override
    public Cache getCache() {
        return this.daoDelegate.getCache();
    }

    /**
	 * @return Objeto que executa de fato funções delegadas a ele.
	 */
	 protected AbstractJpaDAOImpl delegate() {
		 return this.daoDelegate;
	 }

	 /**
	 * @param limits
	 *            Paginaçao.
	 * @param isCount
	 *            Se for uma operação de contagem das ocorrências de uma query.
	 * @param equalAttributes
	 *            Atributos de cláusula where para os casos de igualdade.
	 * @param likeAttributes
	 *            Atributos de cláusula where para os casos de 'like'.
	 * @return Resultado da execução.
	 */
	 protected TypedQuery<T> getQuery(final PageLimits limits,
			 final boolean isCount,
			 final List<SingularAttributeEntry<T, ?>> equalAttributes,
			 final List<SingularAttributeEntry<T, String>> likeAttributes) {
		 return getQuery(this.entityClass, this.entityClass, limits, isCount,
				 equalAttributes, likeAttributes);
	 }

	 /**
	 * @param clasz
	 *            Classe da entidade.
	 * @param resultClass
	 *            Tipo do objetos de retorno.
	 * @param limits
	 *            Limites.
	 * @param isCount
	 *            Se é uma contagem ou uma listagem.
	 * @param equalAttributes
	 *            Atributos da cláusula 'where'. Atributos nulos são
	 *            desconsiderados.
	 * @param likeAttributes
	 *            Atributos da cláusula 'like'. Atributos nulos são
	 *            desconsiderados.
	 * @return Query.
	 */
	 protected <E extends BaseEntity, F> TypedQuery<F> getQuery(
			 final Class<E> clasz, final Class<F> resultClass,
			 final PageLimits limits, final boolean isCount,
			 final List<SingularAttributeEntry<E, ?>> equalAttributes,
			 final List<SingularAttributeEntry<E, String>> likeAttributes) {
		 final EntityManager em = getEntityManager();
		 final CriteriaBuilder cb = em.getCriteriaBuilder();
		 final CriteriaQuery<F> cq = cb.createQuery(resultClass);
		 final Root<E> root = cq.from(clasz);
		 final List<Predicate> predicates = new ArrayList<Predicate>();

		// restrições
		 if (isCount) {
			// Operação segura: método privado, métodos que chamam devem
			// garantir consistência.
			// Não é possível parametrizar CriteriaQuery.
			 final CriteriaQuery cqAux = cq;
			 cqAux.select(cb.count(root));
		 }

		 if (equalAttributes != null) {
			 for (final SingularAttributeEntry<E, ?> attribute : equalAttributes) {
				 predicates.add(cb.equal(root.get(attribute.getKey()),
						 attribute.getValue()));
			 }
		 }

		 if (likeAttributes != null) {
			 for (final SingularAttributeEntry<E, String> attribute : likeAttributes) {
				 predicates.add(cb.like(root.get(attribute.getKey()),
						 "%" + attribute.getValue() + "%"));
			 }
		 }

		 if (!predicates.isEmpty()) {
			 cq.where(predicates.toArray(new Predicate[0]));
		 }

		 final TypedQuery<F> q = em.createQuery(cq);

		 if (!isCount && (limits != null)) {
			 q.setFirstResult(limits.getOffset());
			 q.setMaxResults(limits.getSize());
		 }

		 return q;
	 }
}
